/*
 *  Copyright (C) 2010 Matthias Buch-Kromann <mbk.isv@cbs.dk>
 * 
 *  This file is part of the IncrementalParser package.
 *  
 *  The IncrementalParser program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 * 
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.osdtsystem.incparser.graphs;

import gnu.trove.TIntFloatHashMap;
import gnu.trove.TIntFloatIterator;
import gnu.trove.TIntIntHashMap;
import gnu.trove.TIntIntIterator;
import gnu.trove.TIntObjectHashMap;
import gnu.trove.TIntObjectIterator;

/**
 * A simple graph overlay. A simple graph overlay retains the graph structure
 * of the original graph while allowing an arbitrary number of simple and complex
 * features to receive new values, without affecting the original graph. This
 * class can also be used as a super class for graph-modifying overlays.
 * @author Matthias Buch-Kromann <mbk.isv@cbs.dk>
 */
class GraphOverlay extends AbstractGraph {
    // Parent graph and base graph
    AbstractGraph parentGraph;
    final AbstractBaseGraph baseGraph;

    // Modified feature values and scores
    TIntIntHashMap modifiedIntFeatures = null;
    TIntObjectHashMap modifiedObjectFeatures = null;
    TIntFloatHashMap modifiedScores = new TIntFloatHashMap();
    float score;

    // Class settings: array growth and initial capacity
    static int initialModifiedCapacity = 4;


    GraphOverlay(final AbstractGraph parentGraph) {
        this.parentGraph = parentGraph;
        this.baseGraph = parentGraph._baseGraph();
        this.score = parentGraph.score();
    }

    @Override
    public Object clone() {
        try {
            final GraphOverlay clone = (GraphOverlay) super.clone();
            if (modifiedIntFeatures != null)
                clone.modifiedIntFeatures = (TIntIntHashMap) modifiedIntFeatures.clone();
            if (modifiedObjectFeatures != null)
                clone.modifiedObjectFeatures = modifiedObjectFeatures.clone();
            if (modifiedScores != null)
                clone.modifiedScores = (TIntFloatHashMap) modifiedScores.clone();
            return clone;
        }
        catch (CloneNotSupportedException e) {
            throw new InternalError(e.toString());
        }
    }

    // Clear modifications
    public void clearModifications() {
        if (modifiedIntFeatures != null)
            modifiedIntFeatures.clear();
        if (modifiedObjectFeatures != null)
            modifiedObjectFeatures.clear();
        modifiedScores.clear();
        score = parentGraph.score();
    }

    // Layer, node, and feature counts
    public int layers() {
        return parentGraph.layers();
    }

    public int nodes() {
        return parentGraph.nodes();
    }

    // Local graphs
    public int nodeLayer(int node) {
        return intFeature(node, INTFEATURE_LAYER);
    }

    public AbstractLocalGraph _localGraphRW(int node, int layer) {
        return parentGraph._localGraphRW(_nodeInParentGraph(node), layer);
    }

    public LocalGraph localGraph(int node, int layer) {
        return parentGraph.localGraph(node, layer);
    }

    // Feature getters
    public int intFeatures() {
        return parentGraph.intFeatures();
    }

    public int objectFeatures() {
        return parentGraph.objectFeatures();
    }

    public int intFeature(int node, int feature) {
        int packed = Helper.pack(node, feature);
        if (modifiedIntFeatures != null && modifiedIntFeatures.containsKey(packed))
            return modifiedIntFeatures.get(packed);
        return parentGraph.intFeature(_nodeInParentGraph(node), feature);
    }

    public Object objectFeature(int node, int feature) {
        int packed = Helper.pack(node, feature);
        if (modifiedObjectFeatures != null && modifiedObjectFeatures.containsKey(packed))
            return modifiedObjectFeatures.get(packed);
        return parentGraph.objectFeature(_nodeInParentGraph(node), feature);
    }

    public void setIntFeature(int node, int feature, int value) {
        // Do nothing if new value equals old value
        int oldValue = intFeature(node, feature);
        if (value == oldValue)
            return;

        // Only insert feature if it is different from parent value
        int packed = Helper.pack(node, feature);
        int parentValue = parentGraph.intFeature(_nodeInParentGraph(node), feature);
        if (value == parentValue) {
            if (modifiedIntFeatures != null && modifiedIntFeatures.containsKey(packed))
                modifiedIntFeatures.remove(packed);
        } else {
            if (modifiedIntFeatures == null)
                modifiedIntFeatures = new TIntIntHashMap(initialModifiedCapacity);
            modifiedIntFeatures.put(packed, value);
        }

        // Notify listeners about the change
        baseGraph._notifyIntFeatureChanged(this, node, feature, value, oldValue);
    }

    public void setObjectFeature(int node, int feature, Object value) {
        // Do nothing if new value equals old value
        Object oldValue = objectFeature(node, feature);
        if (value == oldValue || (value != null && value.equals(oldValue)))
            return;

        // Only insert feature if it is different from parent value
        int packed = Helper.pack(node, feature);
        Object parentValue = parentGraph.objectFeature(_nodeInParentGraph(node), feature);
        if (value == parentValue || (value != null && value.equals(parentValue))) {
            if (modifiedObjectFeatures != null && modifiedObjectFeatures.containsKey(packed))
                modifiedObjectFeatures.remove(packed);
        } else {
            if (modifiedObjectFeatures == null)
                modifiedObjectFeatures = new TIntObjectHashMap(initialModifiedCapacity);
            modifiedObjectFeatures.put(packed, value);
        }

        // Notify listeners about the change
        baseGraph._notifyObjectFeatureChanged(this, node, feature, value, oldValue);
    }

    // Scores
    public int scorers() {
        return parentGraph.scorers();
    }

    public GraphScorer scorer(int scorer) {
        return _baseGraph().scorer(scorer);
    }

    public float score() {
        // Ensure that all parent scores are updated
        parentGraph.score();

        // Ensure that all local scores are updated
        for (int packed : modifiedScores.keys()) {
            int node = Helper.nodePart(packed);
            int scorer = Helper.scorerPart(packed);
            if (modifiedScores.get(packed) == Float.NaN)
                localScore(node, scorer);
        }

        // Return updated score
        return score;
    }

    public float localScore(int node, int scorer) {
        int packed = Helper.pack(node, scorer);
        if (modifiedScores.containsKey(packed)) {
            // Compute score locally
            float localScore = modifiedScores.get(packed);
            if (localScore == Float.NaN) {
                localScore = _baseGraph().scorer(scorer).score(this, node);
                modifiedScores.put(packed, localScore);
                parentGraph._updateLocalScoreBest(node, scorer, localScore);
                score += localScore;
            }
            return localScore;
        } else {
            // Return score from parent
            return localScoreParent(node, scorer);
        }
    }

    public float localScoreParent(int node, int scorer) {
        return parentGraph.localScore(_nodeInParentGraph(node), scorer);
    }

    public float localScoreBase(int node, int scorer) {
        return parentGraph.localScoreBase(_nodeInParentGraph(node), scorer);
    }

    public float localScoreBest(int node, int scorer) {
        return parentGraph.localScoreBest(_nodeInParentGraph(node), scorer);
    }

    public void outdateScore(int node, int scorer) {
        // Adjust global score with old local score
        int packed = Helper.pack(node, scorer);
        if (modifiedScores.containsKey(packed)) {
            float oldScore = modifiedScores.get(packed);
            if (oldScore != Float.NaN)
                score -= oldScore;
        } else {
            score -= localScoreParent(node, scorer);
        }

        // Mark local score as outdated
        modifiedScores.put(packed, Float.NaN);
    }

    public void _updateLocalScoreBest(int node, int scorer, float localScore) {
        parentGraph._updateLocalScoreBest(_nodeInParentGraph(node), scorer, localScore);
    }

    public void _setLocalScore(int packed, float score) {
        throw new UnsupportedOperationException("Setting local scores is only supported in base graphs");
    }

    // Graph operations
    public GraphOverlayAddNode makeOverlayAddNode(int layer) {
        return new GraphOverlayAddNode(this, layer);
    }

    public GraphOverlayAddEdge makeOverlayAddEdge(int layer, int innode, int outnode, int type) {
        return new GraphOverlayAddEdge(this, layer, innode, outnode, type);
    }

    public GraphOverlayRemoveEdge makeOverlayRemoveEdge(int layer, int innode, int outnode, int type) {
        return new GraphOverlayRemoveEdge(this, layer, innode, outnode, type);
    }

    public GraphOverlayMoveEdge makeOverlayMoveEdge(int layer, int innode, int oldoutnode, int oldtype, int newoutnode, int newtype) {
        return new GraphOverlayMoveEdge(this, layer, innode, oldoutnode, oldtype, newoutnode, newtype);
    }

    // Node translation
    int _nodeInParentGraph(int node) {
        return (0 <= node && node < nodes()) ? node : -1;
    }

    // Base graph, graph trimming, and cloning
    public AbstractBaseGraph _baseGraph() {
        return baseGraph;
    }

    void _writeGraphChangesToBaseGraphPreprocess() {
    }

    public AbstractGraph _writeToBaseGraphRW() {
        // Turn notification off in base graph
        boolean baseGraphNotification = baseGraph._isNotificationOn();
        baseGraph._notifyOff();

        // Write parent to base graph
        parentGraph = parentGraph._writeToBaseGraphRW();
        
        // Perform preprocessing
        _writeGraphChangesToBaseGraphPreprocess();

        // Write all modified simple feature values to base
        if (modifiedIntFeatures != null) {
            for (TIntIntIterator it = modifiedIntFeatures.iterator(); it.hasNext(); ) {
                it.advance();
                int packed = it.key();
                baseGraph.setIntFeature(Helper.nodePart(packed), Helper.featurePart(packed),
                        it.value());
            }
        }

        // Write all modified complex feature values to base
        if (modifiedObjectFeatures != null) {
            for (TIntObjectIterator it = modifiedObjectFeatures.iterator(); it.hasNext(); ) {
                it.advance();
                int packed = it.key();
                baseGraph.setObjectFeature(Helper.nodePart(packed), Helper.featurePart(packed),
                        it.value());
            }
        }
        
        // Write all modified scores to base
        for (TIntFloatIterator it = modifiedScores.iterator(); it.hasNext(); ) {
            it.advance();
            int packed = it.key();
            baseGraph._setLocalScore(Helper.nodePart(packed), Helper.scorerPart(packed),
                    it.value());
        }

        // Restore base graph notification and return base graph
        if (baseGraphNotification)
            baseGraph._notifyOn();
        return baseGraph;
    }

    public Graph writeToBaseGraph() {
        return _writeToBaseGraphRW();
    }
    
    // Trimming arrays
    public void trimToSize() {
        modifiedIntFeatures.trimToSize();
        modifiedObjectFeatures.trimToSize();
        modifiedScores.trimToSize();
    }

    // Unsupported operations (only valid on the base graph)
    public int addIntFeature(String name) {
        throw new UnsupportedOperationException("Graph overlays do not support the adding of features");
    }

    public int addObjectFeature(String name) {
        throw new UnsupportedOperationException("Graph overlays do not support the adding of features");
    }

    public void addGraphListener(GraphListener listener, int[] layers) {
        throw new UnsupportedOperationException("Graph overlays do not support the adding of graph listeners");
    }

    public void addIntFeatureListener(GraphListener listener, int[] features) {
        throw new UnsupportedOperationException("Graph overlays do not support the adding of feature listeners");
    }

    public void addObjectFeatureListener(GraphListener listener, int[] features) {
        throw new UnsupportedOperationException("Graph overlays do not support the adding of feature listeners");
    }

    public int addLayer(String name) {
        throw new UnsupportedOperationException("Graph overlays do not support the adding of layers");
    }

    public int addGraphScorer(GraphScorer scorer) {
        throw new UnsupportedOperationException("Graph overlays do not support the adding of scorers");
    }

    // Notification
    public void _notifyOn() {
    }

    public void _notifyOff() {
        throw new UnsupportedOperationException("Notification cannot be turned off in graph overlays");
    }

    public boolean _isNotificationOn() {
        return true;
    }

}
